﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Threading;

// ReSharper disable StaticFieldInGenericType
namespace N3P.WCF.Proxyless
{
    /// <summary>
    /// Utility class for generating the <see cref="T:System.ServiceModel.ClientBase`1" /> derived async WCF client
    /// </summary>
    /// <typeparam name="T">The type of the synchronous service contract to generate an async client for</typeparam>
    [DebuggerNonUserCode]
    internal static class AsyncClientCreator<T>
    {
		#region Fields (10) 

        /// <summary>
        /// The contructor for the client that accepts a <see cref="T:System.ServiceModel.Channels.Binding" /> and <see cref="T:System.ServiceModel.EndpointAddress" />
        /// </summary>
        private static Func<Binding, EndpointAddress, object> _bindingEndpointAddressCtor;
        /// <summary>
        /// The type of the generated client
        /// </summary>
        private static Type _clientType;
        /// <summary>
        /// The default constructor for the client
        /// </summary>
        private static Func<object> _defaultCtor;
        /// <summary>
        /// The type of the generated asynchronous contract generated for the synchronous contract
        /// </summary>
        private static Type _interfaceType;
        /// <summary>
        /// The constructor that accepts a <see cref="T:System.String" />
        /// </summary>
        private static Func<string, object> _stringCtor;
        /// <summary>
        /// The constructor that accepts a <see cref="T:System.String" /> and a <see cref="System.ServiceModel.EndpointAddress" />
        /// </summary>
        private static Func<string, EndpointAddress, object> _stringEndpointAddressCtor;
        /// <summary>
        /// The constructor that accepts a pair of <see cref="T:System.String" /> parameters
        /// </summary>
        private static Func<string, string, object> _stringStringCtor;
        /// <summary>
        /// The map of synchronous operations to the async begin methods for this client
        /// </summary>
        internal static Dictionary<MethodInfo, MethodInfo> BeginMethodMap = new Dictionary<MethodInfo, MethodInfo>();
        /// <summary>
        /// The map of synchronous operations to the async end methods for this client
        /// </summary>
        internal static Dictionary<MethodInfo, MethodInfo> EndMethodMap = new Dictionary<MethodInfo, MethodInfo>();
        /// <summary>
        /// Thread sync to ensure that the client type is not defined more than once
        /// </summary>
        private static readonly object Sync = new object();

		#endregion Fields 

		#region Properties (1) 

        /// <summary>
        /// Gets the method used to close the channel for the client
        /// </summary>
        public static Action<object> CloseMethod { get; private set; }

		#endregion Properties 

		#region Methods (8) 

		// Public Methods (5) 

        /// <summary>
        /// Gets an instance of the client using the default constructor
        /// </summary>
        /// <returns>An instance of the client</returns>
        public static object GetClient()
        {
            EnsureClientDefinition();
            return _defaultCtor();
        }

        /// <summary>
        /// Gets an instance of the client using the constructor that accepts the name of the binding to use
        /// </summary>
        /// <param name="bindingName">The name of the binding to use</param>
        /// <returns>An instance of the client</returns>
        public static object GetClient(string bindingName)
        {
            EnsureClientDefinition();
            return _stringCtor(bindingName);
        }

        /// <summary>
        /// Gets an instance of the client using the constructor that accepts the name of the binding to use and the remote address 
        /// </summary>
        /// <param name="bindingName">The name of the binding to use</param>
        /// <param name="remoteAddress">The address of the service</param>
        /// <returns>An instance of the client</returns>
        public static object GetClient(string bindingName, string remoteAddress)
        {
            EnsureClientDefinition();
            return _stringStringCtor(bindingName, remoteAddress);
        }

        /// <summary>
        /// Gets an instance of the client using the constructor that accepts the name of the binding to use and the remote address
        /// </summary>
        /// <param name="bindingName">The name of the binding to use</param>
        /// <param name="remoteAddress">The address of the service</param>
        /// <returns>An instance of the client</returns>
        public static object GetClient(string bindingName, EndpointAddress remoteAddress)
        {
            EnsureClientDefinition();
            return _stringEndpointAddressCtor(bindingName, remoteAddress);
        }

        /// <summary>
        /// Gets an instance of the client using the constructor that accepts the binding to use and the remote address
        /// </summary>
        /// <param name="binding">The binding to use</param>
        /// <param name="remoteAddress">The address of the service</param>
        /// <returns>An instance of the client</returns>
        public static object GetClient(Binding binding, EndpointAddress remoteAddress)
        {
            EnsureClientDefinition();
            return _bindingEndpointAddressCtor(binding, remoteAddress);
        }
		// Private Methods (2) 

        /// <summary>
        /// Emits a method into the type being defined for the client that passes the call through to the underlying channel
        /// </summary>
        /// <param name="defineIn">The type to define the method in</param>
        /// <param name="prefix">The prefix for the method name (Begin or End)</param>
        /// <param name="method">The synchronous method to maintain compatibility with</param>
        /// <param name="channelPropertyGetter">The get method for the <see cref="P:System.ServiceModel.ClientBase`1.Channel" /> property</param>
        /// <param name="placeInLookup">The lookup to place the name of the generated method into so that it may be retrieved after the type is created</param>
        private static void EmitNamedPassthroughMethod(TypeBuilder defineIn, string prefix, MethodInfo method, MethodInfo channelPropertyGetter, ref Dictionary<MethodInfo, string> placeInLookup)
        {
            var asyncMethod = _interfaceType.GetMethod(prefix + method.Name);

            if(asyncMethod == null)
            {
                return;
            }

            var mParams = asyncMethod.GetParameters().Select(x => x.ParameterType).ToArray();
            var methodBuilder = defineIn.DefineMethod(asyncMethod.Name, MethodAttributes.Public, asyncMethod.CallingConvention, asyncMethod.ReturnType, mParams);
            var generator = methodBuilder.GetILGenerator();
            generator.Emit(OpCodes.Ldarg_0);
            generator.EmitCall(OpCodes.Callvirt, channelPropertyGetter, null);

            for (var i = 0; i < mParams.Length; i++)
            {
                generator.Emit(OpCodes.Ldarg, i + 1);
            }
            generator.EmitCall(OpCodes.Callvirt, asyncMethod, null);
            generator.Emit(OpCodes.Ret);

            placeInLookup[method] = methodBuilder.Name;
        }

        /// <summary>
        /// Extracts the common members from the generated <see cref="_clientType" /> (such as the constructors and the Close method) and 
        /// places them in their respective caches
        /// </summary>
        private static void ExtractCommonClientInformation()
        {
            // ReSharper disable PossibleNullReferenceException
            var defaultCtor = _clientType.GetConstructor(Type.EmptyTypes);
            _defaultCtor = () => defaultCtor.Invoke(null);
            var stringCtor = _clientType.GetConstructor(new[] { typeof(string) });
            _stringCtor = b => stringCtor.Invoke(new object[] { b });
            var stringStringCtor = _clientType.GetConstructor(new[] { typeof(string), typeof(string) });
            _stringStringCtor = (b, a) => stringStringCtor.Invoke(new object[] { b, a });
            var stringEndpointAddressCtor = _clientType.GetConstructor(new[] { typeof(string), typeof(EndpointAddress) });
            _stringEndpointAddressCtor = (b, a) => stringEndpointAddressCtor.Invoke(new object[] { b, a });
            var bindingEndpointAddressCtor = _clientType.GetConstructor(new[] { typeof(Binding), typeof(EndpointAddress) });
            _bindingEndpointAddressCtor = (b, a) => bindingEndpointAddressCtor.Invoke(new object[] { b, a });
            var closeMethod = _clientType.GetMethod("Close");
            CloseMethod = closeMethod != null ? o => closeMethod.Invoke(o, new object[0]) : new Action<object>(o => { });
            // ReSharper restore PossibleNullReferenceException
        }
		// Internal Methods (1) 

        /// <summary>
        /// Ensures that the client type has been defined
        /// </summary>
        internal static void EnsureClientDefinition()
        {
            //If our client type has already been defined, return
            if (Interlocked.CompareExchange(ref _clientType, null, null) != null)
            {
                return;
            }

            //Process exactly one client definition creation at a time
            lock (Sync)
            {
                //If our client type has already been defined, return
                if (Interlocked.CompareExchange(ref _clientType, null, null) != null)
                {
                    return;
                }

                //If our async interface type has not yet been defined...
                if (Interlocked.CompareExchange(ref _interfaceType, null, null) == null)
                {
                    Interlocked.Exchange(ref _interfaceType, AsyncContractCreator.Create<T>());
                }

                var specializedClientBaseType = typeof(ClientBase<>).MakeGenericType(_interfaceType);
                var clientBuilder = SharedResources.Module.DefineType(typeof (T).FullName + "Client", TypeAttributes.Class | TypeAttributes.Public, specializedClientBaseType);

                clientBuilder.DefineDefaultConstructor(MethodAttributes.Public);

                var stringCtor = clientBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] {typeof (string)});
                var generator = stringCtor.GetILGenerator();
                var baseStringCtor = specializedClientBaseType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string) }, null);
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Call, baseStringCtor);
                generator.Emit(OpCodes.Ret);

                var stringStringCtor = clientBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(string), typeof(string) });
                generator = stringStringCtor.GetILGenerator();
                var baseStringStringCtor = specializedClientBaseType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string), typeof(string) }, null);
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Ldarg_2);
                generator.Emit(OpCodes.Call, baseStringStringCtor);
                generator.Emit(OpCodes.Ret);

                var stringEpaCtor = clientBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(string), typeof(EndpointAddress) });
                generator = stringEpaCtor.GetILGenerator();
                var baseStringEpaCtor = specializedClientBaseType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(string), typeof(EndpointAddress) }, null);
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Ldarg_2);
                generator.Emit(OpCodes.Call, baseStringEpaCtor);
                generator.Emit(OpCodes.Ret);

                var bindingEpaCtor = clientBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(Binding), typeof(EndpointAddress) });
                generator = bindingEpaCtor.GetILGenerator();
                var baseBindingEpaCtor = specializedClientBaseType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new[] { typeof(Binding), typeof(EndpointAddress) }, null);
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Ldarg_2);
                generator.Emit(OpCodes.Call, baseBindingEpaCtor);
                generator.Emit(OpCodes.Ret);

                var channelPropGet = specializedClientBaseType.GetProperty("Channel", BindingFlags.NonPublic | BindingFlags.Instance).GetGetMethod(true);

                var beginFetchDict = new Dictionary<MethodInfo, string>();
                var endFetchDict = new Dictionary<MethodInfo, string>();

                var concreteMethods = typeof (T).GetMethods().ToList();
                concreteMethods.AddRange(typeof (T).GetInterfaces().SelectMany(x => x.GetMethods()));

                foreach(var method in concreteMethods.Distinct())
                {
                    if(!method.IsDefined(SharedResources.OperationContractAttributeType, true))
                    {
                        continue;
                    }

                    EmitNamedPassthroughMethod(clientBuilder, "Begin", method, channelPropGet, ref beginFetchDict);
                    EmitNamedPassthroughMethod(clientBuilder, "End", method, channelPropGet, ref endFetchDict);
                }

                Interlocked.Exchange(ref _clientType, clientBuilder.CreateType());

                BeginMethodMap = beginFetchDict.ToDictionary(x => x.Key, x => _clientType.GetMethod(x.Value));
                EndMethodMap = endFetchDict.ToDictionary(x => x.Key, x => _clientType.GetMethod(x.Value));

                ExtractCommonClientInformation();
            }
        }

		#endregion Methods 
    }
}
// ReSharper restore StaticFieldInGenericType
